package algorithms.disCBJ;

import general.Agent;
import general.ChannelAgent;
import general.NotImplementedException;
import general.CPA.CPA;
import general.conflictset.ArrayConflictSet;
import general.conflictset.ConflictSet;
import general.constraints.VariableConstraints;
import general.currentdomain.ArrayCurrentDomain;
import general.currentdomain.CurrentDomain;
import general.messages.BacktrackMessage;
import general.messages.CPAMessage;
import general.messages.ConflictMessage;
import general.messages.FailMessage;
import general.messages.Message;
import general.messages.RestoreMessage;
import general.messages.SplitMessage;
import general.messages.SuccessMessage;

import java.util.GregorianCalendar;

import ordering.VariableOrdering;
import priorities.PriorityManager;

public class DisCBJAgent extends ChannelAgent implements Agent{

	public static int BACKTRACK = 1;
	public static int FAIL = 2;
	public static int SUCCESS = 3;
	public static int FORWARD = 4;

	private long startTime;

	CurrentDomain current_domain;
	ConflictSet conflicts;		//true - conflicted, false - not conflicted
	VariableOrdering ordering;
	CPA cpa;
	String solution;


	public DisCBJAgent(byte id, byte domain, int n, VariableConstraints constraints, VariableOrdering ordering, CPA cpaProto, int delay) {
		super(id, n, constraints, cpaProto, delay);
		this.constraints = constraints;
		this.conflicts = new ArrayConflictSet(n);
		this.current_domain = new ArrayCurrentDomain(domain);
		this.ordering = ordering;
	}

	@Override
	public void run() {
		startTime = GregorianCalendar.getInstance().getTimeInMillis();
		log("I'm running", LOG_STARTRUN);
		loop();
		long totalTime = GregorianCalendar.getInstance().getTimeInMillis() - startTime;
		log("Total runtime: " + (totalTime) +" miliseconds", LOG_FINISHRUN);
//		close();
	}

	private int receiveCPA(CPA cpa){
		this.cpa = cpa; 
		boolean consistent = false; 							//1
		for (Integer i: current_domain){						//2
			consistent = true;									//2.a
			byte myVal = i.byteValue();
			for (Integer j: cpa){								//2.b
				byte other = j.byteValue();
				byte otherVal = cpa.getValue(other);
				consistent = check(myVal, other, otherVal);		//2.b.i
				if (!consistent){								//2.b.ii
					conflicts.insert(j.byteValue());
					current_domain.remove(i.byteValue());	
					break;
				}
			}
			if (consistent){									//2.c
				cpa.insert(id, myVal);							//2.c.i
				break;
			}
		}
		if (consistent){										//3
			if (ordering.hasNext(cpa, id)){
				byte nextAgent = ordering.next(cpa, id, constraints); 
				log("Sending CPA to next("+nextAgent+") - " + cpa , LOG_SENDNEXT);
				send(cpa, nextAgent);
				return FORWARD;
			} else {
				log("SUCCESS!!! - " + cpa.toString(), LOG_SUCCESS);
				success(cpa);
				return SUCCESS;				
			}
		} else {
			byte h = ordering.max(cpa, id, conflicts);
			if (ordering.isFirst(id) || h==-1){
				log("We failed, informing everyone", LOG_FAIL);
				fail();
				return FAIL;
			}
			log("Backtracking - " + cpa, LOG_BACKTRACK);
			backtrack(cpa, h);
			return BACKTRACK;
		}
	}

	private void fail(){
		solution = null;
		for (byte i=0; i<n; i++) if (i!=id){
				sendFail(i);
		}
	}
	
	private void success(CPA p){
		solution = p.toString();
		for (byte i=0; i<n; i++) if (i!=id) {
				sendSuccess(p, i);
		}
	}

	private void backtrack(CPA p, byte h){		//1
		if (h==-1) System.out.println("Agent " + id + ": Conflicts empty " + conflicts);
		ordering.removeBigger(p, conflicts, h);
		conflicts.remove(h);
		send(conflicts, p, h);						//2
		byte[] bt = ordering.between(p, h, id);
		for (int i = 0; i < bt.length; i++) {	//3
			sendRestore(bt[i]);					//3.a
		}
		current_domain.reset();					//4
		conflicts.reset();
		ordering.removeBigger(p, h);
		send(p, h);
	}

	public boolean handleMessage(CPAMessage m){
		log("Handling CPAMessage - " + m.getCPA(), LOG_HANDLE_CPA);
		int status = receiveCPA(m.getCPA());
		if (status == BACKTRACK || status == FORWARD) return true;
		else return false;
	}

	public boolean handleMessage(ConflictMessage m){
		log("Handling ConflictMessage - " + m.getConflicts(), LOG_HANDLE_CONFLICTS);
		current_domain.remove(cpa.getValue(id));
		conflicts.addAll(m.getConflicts());
		return true;
	}

	public boolean handleMessage(RestoreMessage m){
		log("Handling RestoreMessage", LOG_HANDLE_RESTORE);
		conflicts.reset();
		current_domain.reset();
		return true;
	}

	public boolean handleMessage(SuccessMessage successMessage) {
		log("Success detetected - " + successMessage.getResult(), LOG_HANDLE_SUCCESS);
		solution = successMessage.getResult();
		return false;
	}
	public boolean handleMessage(FailMessage successMessage) {
		log("Failure detetected", LOG_HANDLE_FAIL);
		solution = null;
		return false;
	}
	
	public boolean handleMessage(SplitMessage splitMessage) {
		new NotImplementedException("DiscCBJ does not support split messages").printStackTrace();;
		return true;
	}
	
	public boolean handleMessage(BacktrackMessage backtrackMessage) {
		new NotImplementedException("DiscCBJ does not support backtrack messages").printStackTrace();;
		return true;
	}

	private void loop(){
		if (ordering.isFirst(id)){
			receiveCPA(getNewCPA(n, 0, id));
		}
		boolean done = false;
		while(!done){
			Message m = receive();
			done = !m.handle(this);
			PriorityManager.getInstance().updatePriority(this);
		}
	}
	//TODO
	//TODO
	//TODO
	//TODO
	//TODO
	//possible way of keeping the order, is pass the timestamp between the variables.
	//every time a variable recieves a message, it updates his timestamp and adds 1 to it (not for ALL types of messages, only a CPA message)
	//the only way to do it is probably making a centralized agent which is responsible for passing the messages.

	//TODO
	public String getSolution() {
		return solution;
	}

	//also need to make sure that when receiving a CPA, we only treat the variables lower then us.
}
